/*
 * Copyright OpenSearch Contributors
 * SPDX-License-Identifier: Apache-2.0
 */

package org.opensearch.sql.legacy.parser;

import static org.opensearch.sql.legacy.utils.StringUtils.isQuoted;

import com.alibaba.druid.sql.ast.SQLCommentHint;
import com.alibaba.druid.sql.ast.SQLDataType;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLName;
import com.alibaba.druid.sql.ast.SQLOrderBy;
import com.alibaba.druid.sql.ast.SQLOrderingSpecification;
import com.alibaba.druid.sql.ast.expr.SQLAggregateExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOperator;
import com.alibaba.druid.sql.ast.expr.SQLCharExpr;
import com.alibaba.druid.sql.ast.expr.SQLDefaultExpr;
import com.alibaba.druid.sql.ast.expr.SQLExistsExpr;
import com.alibaba.druid.sql.ast.expr.SQLHexExpr;
import com.alibaba.druid.sql.ast.expr.SQLIdentifierExpr;
import com.alibaba.druid.sql.ast.expr.SQLIntegerExpr;
import com.alibaba.druid.sql.ast.expr.SQLMethodInvokeExpr;
import com.alibaba.druid.sql.ast.expr.SQLNotExpr;
import com.alibaba.druid.sql.ast.expr.SQLPropertyExpr;
import com.alibaba.druid.sql.ast.expr.SQLUnaryExpr;
import com.alibaba.druid.sql.ast.expr.SQLUnaryOperator;
import com.alibaba.druid.sql.ast.expr.SQLVariantRefExpr;
import com.alibaba.druid.sql.ast.statement.SQLAssignItem;
import com.alibaba.druid.sql.ast.statement.SQLColumnDefinition;
import com.alibaba.druid.sql.dialect.mysql.ast.MySqlPrimaryKey;
import com.alibaba.druid.sql.dialect.mysql.ast.MySqlUnique;
import com.alibaba.druid.sql.dialect.mysql.ast.MysqlForeignKey;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlCharExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlExtractExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlIntervalExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlIntervalUnit;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlMatchAgainstExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlOutFileExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlSelectGroupByExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlUserName;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlSQLColumnDefinition;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlSelectQueryBlock;
import com.alibaba.druid.sql.parser.Lexer;
import com.alibaba.druid.sql.parser.ParserException;
import com.alibaba.druid.sql.parser.SQLExprParser;
import com.alibaba.druid.sql.parser.SQLSelectParser;
import com.alibaba.druid.sql.parser.Token;
import com.alibaba.druid.util.JdbcConstants;
import java.util.List;

/** Created by Eliran on 18/8/2015. */
public class ElasticSqlExprParser extends SQLExprParser {
  public ElasticSqlExprParser(Lexer lexer) {
    super(lexer);
    this.aggregateFunctions = AGGREGATE_FUNCTIONS;
  }

  public ElasticSqlExprParser(String sql) {
    this(new ElasticLexer(sql));
    this.lexer.nextToken();
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Override
  public void parseHints(List hints) {
    while (lexer.token() == Token.HINT) {
      hints.add(new SQLCommentHint(lexer.stringVal()));
      lexer.nextToken();
    }
  }

  @Override
  protected SQLExpr methodRest(SQLExpr expr, boolean acceptLPAREN) {
    if (acceptLPAREN) {
      accept(Token.LPAREN);
    }

    if (expr instanceof SQLName || expr instanceof SQLDefaultExpr) {
      String methodName;

      SQLMethodInvokeExpr methodInvokeExpr;
      if (expr instanceof SQLPropertyExpr) {
        methodName = ((SQLPropertyExpr) expr).getName();
        methodInvokeExpr = new SQLMethodInvokeExpr(methodName);
        methodInvokeExpr.setOwner(((SQLPropertyExpr) expr).getOwner());
      } else {
        methodName = expr.toString();
        methodInvokeExpr = new SQLMethodInvokeExpr(methodName);
      }

      if (isAggreateFunction(methodName)) {
        SQLAggregateExpr aggregateExpr = parseAggregateExpr(methodName);

        return aggregateExpr;
      }

      if (lexer.token() != Token.RPAREN) {
        exprList(methodInvokeExpr.getParameters(), methodInvokeExpr);
      }

      accept(Token.RPAREN);

      return primaryRest(methodInvokeExpr);
    }

    throw new ParserException("Syntax error: " + lexer.token());
  }

  public SQLExpr primary() {

    if (lexer.token() == Token.LBRACE) {
      lexer.nextToken();
      boolean foundRBrace = false;
      if (lexer.stringVal().equals("ts")) {
        String current = lexer.stringVal();
        do {
          if (current.equals(lexer.token().RBRACE.name())) {
            foundRBrace = true;
            break;
          }
          lexer.nextToken();
          current = lexer.token().name();
        } while (!foundRBrace && !current.trim().equals(""));

        if (foundRBrace) {
          SQLOdbcExpr sdle = new SQLOdbcExpr(lexer.stringVal());

          accept(Token.RBRACE);
          return sdle;
        } else {
          throw new ParserException("Error. Unable to find closing RBRACE");
        }
      } else {
        throw new ParserException("Error. Unable to parse ODBC Literal Timestamp");
      }
    } else if (lexer.token() == Token.LBRACKET) {
      StringBuilder identifier = new StringBuilder();
      lexer.nextToken();
      String prefix = "";
      while (lexer.token() != Token.RBRACKET) {
        if (lexer.token() != Token.IDENTIFIER
            && lexer.token() != Token.INDEX
            && lexer.token() != Token.LITERAL_CHARS) {
          throw new ParserException(
              "All items between Brackets should be identifiers , got:" + lexer.token());
        }
        identifier.append(prefix);
        identifier.append(lexer.stringVal());
        prefix = " ";
        lexer.nextToken();
      }

      accept(Token.RBRACKET);
      return new SQLIdentifierExpr(identifier.toString());
    } else if (lexer.token() == Token.NOT) {
      lexer.nextToken();
      SQLExpr sqlExpr;
      if (lexer.token() == Token.EXISTS) {
        lexer.nextToken();
        accept(Token.LPAREN);
        sqlExpr = new SQLExistsExpr(createSelectParser().select(), true);
        accept(Token.RPAREN);
      } else if (lexer.token() == Token.LPAREN) {
        lexer.nextToken();

        SQLExpr notTarget = expr();

        accept(Token.RPAREN);

        sqlExpr = new SQLNotExpr(notTarget);

        return primaryRest(sqlExpr);
      } else {
        SQLExpr restExpr = relational();
        sqlExpr = new SQLNotExpr(restExpr);
      }
      return sqlExpr;
    }

    boolean parenWrapped = lexer.token() == Token.LPAREN;

    SQLExpr expr = primary2();

    // keep track of if the identifier is wrapped in parens
    if (parenWrapped && expr instanceof SQLIdentifierExpr) {
      expr = new SQLParensIdentifierExpr((SQLIdentifierExpr) expr);
    }

    return expr;
  }

  public static String[] AGGREGATE_FUNCTIONS = {
    "AVG", "COUNT", "GROUP_CONCAT", "MAX", "MIN", "STDDEV", "SUM"
  };

  public SQLExpr relationalRest(SQLExpr expr) {
    if (identifierEquals("REGEXP")) {
      lexer.nextToken();
      SQLExpr rightExp = equality();

      rightExp = relationalRest(rightExp);

      return new SQLBinaryOpExpr(expr, SQLBinaryOperator.RegExp, rightExp, JdbcConstants.MYSQL);
    }

    return super.relationalRest(expr);
  }

  public SQLExpr multiplicativeRest(SQLExpr expr) {
    if (lexer.token() == Token.IDENTIFIER && "MOD".equalsIgnoreCase(lexer.stringVal())) {
      lexer.nextToken();
      SQLExpr rightExp = primary();

      rightExp = relationalRest(rightExp);

      return new SQLBinaryOpExpr(expr, SQLBinaryOperator.Modulus, rightExp, JdbcConstants.MYSQL);
    }

    return super.multiplicativeRest(expr);
  }

  public SQLExpr notRationalRest(SQLExpr expr) {
    if (identifierEquals("REGEXP")) {
      lexer.nextToken();
      SQLExpr rightExp = primary();

      rightExp = relationalRest(rightExp);

      return new SQLBinaryOpExpr(expr, SQLBinaryOperator.NotRegExp, rightExp, JdbcConstants.MYSQL);
    }

    return super.notRationalRest(expr);
  }

  public SQLExpr primary2() {
    final Token tok = lexer.token();

    if (identifierEquals("outfile")) {
      lexer.nextToken();
      SQLExpr file = primary();
      SQLExpr expr = new MySqlOutFileExpr(file);

      return primaryRest(expr);
    }

    switch (tok) {
      case LITERAL_ALIAS:
        String aliasValue = lexer.stringVal();
        lexer.nextToken();
        return primaryRest(new SQLCharExpr(aliasValue));
      case VARIANT:
        SQLVariantRefExpr varRefExpr = new SQLVariantRefExpr(lexer.stringVal());
        lexer.nextToken();
        if (varRefExpr.getName().equalsIgnoreCase("@@global")) {
          accept(Token.DOT);
          varRefExpr = new SQLVariantRefExpr(lexer.stringVal(), true);
          lexer.nextToken();
        } else if (varRefExpr.getName().equals("@") && lexer.token() == Token.LITERAL_CHARS) {
          varRefExpr.setName("@'" + lexer.stringVal() + "'");
          lexer.nextToken();
        } else if (varRefExpr.getName().equals("@@") && lexer.token() == Token.LITERAL_CHARS) {
          varRefExpr.setName("@@'" + lexer.stringVal() + "'");
          lexer.nextToken();
        }
        return primaryRest(varRefExpr);
      case VALUES:
        lexer.nextToken();
        if (lexer.token() != Token.LPAREN) {
          throw new ParserException("Syntax error: " + lexer.token());
        }
        return this.methodRest(new SQLIdentifierExpr("VALUES"), true);
      case BINARY:
        lexer.nextToken();
        if (lexer.token() == Token.COMMA
            || lexer.token() == Token.SEMI
            || lexer.token() == Token.EOF) {
          return new SQLIdentifierExpr("BINARY");
        } else {
          SQLUnaryExpr binaryExpr = new SQLUnaryExpr(SQLUnaryOperator.BINARY, expr());
          return primaryRest(binaryExpr);
        }
      case CACHE:
      case GROUP:
        lexer.nextToken();
        return primaryRest(new SQLIdentifierExpr(lexer.stringVal()));
      case DOT:
        lexer.nextToken();
        return primaryRest(new SQLIdentifierExpr("." + lexer.stringVal()));
      default:
        return super.primary();
    }
  }

  public final SQLExpr primaryRest(SQLExpr expr) {
    if (expr == null) {
      throw new IllegalArgumentException("Illegal expression: NULL");
    }

    if (lexer.token() == Token.LITERAL_CHARS) {
      if (expr instanceof SQLIdentifierExpr) {
        SQLIdentifierExpr identExpr = (SQLIdentifierExpr) expr;
        String ident = identExpr.getName();

        if (ident.equalsIgnoreCase("x")) {
          String charValue = lexer.stringVal();
          lexer.nextToken();
          expr = new SQLHexExpr(charValue);

          return primaryRest(expr);
        } else if (ident.equalsIgnoreCase("b")) {
          String charValue = lexer.stringVal();
          lexer.nextToken();
          expr = new SQLBinaryExpr(charValue);

          return primaryRest(expr);
        } else if (ident.startsWith("_")) {
          String charValue = lexer.stringVal();
          lexer.nextToken();

          MySqlCharExpr mysqlCharExpr = new MySqlCharExpr(charValue);
          mysqlCharExpr.setCharset(identExpr.getName());
          if (identifierEquals("COLLATE")) {
            lexer.nextToken();

            String collate = lexer.stringVal();
            mysqlCharExpr.setCollate(collate);
            accept(Token.IDENTIFIER);
          }

          expr = mysqlCharExpr;

          return primaryRest(expr);
        }
      } else if (expr instanceof SQLCharExpr) {
        SQLMethodInvokeExpr concat = new SQLMethodInvokeExpr("CONCAT");
        concat.addParameter(expr);
        do {
          String chars = lexer.stringVal();
          concat.addParameter(new SQLCharExpr(chars));
          lexer.nextToken();
        } while (lexer.token() == Token.LITERAL_CHARS || lexer.token() == Token.LITERAL_ALIAS);
        expr = concat;
      }
    } else if (lexer.token() == Token.IDENTIFIER) {
      if (expr instanceof SQLHexExpr) {
        if ("USING".equalsIgnoreCase(lexer.stringVal())) {
          lexer.nextToken();
          if (lexer.token() != Token.IDENTIFIER) {
            throw new ParserException("Syntax error: " + lexer.token());
          }
          String charSet = lexer.stringVal();
          lexer.nextToken();
          expr.getAttributes().put("USING", charSet);

          return primaryRest(expr);
        }
      } else if ("COLLATE".equalsIgnoreCase(lexer.stringVal())) {
        lexer.nextToken();

        if (lexer.token() == Token.EQ) {
          lexer.nextToken();
        }

        if (lexer.token() != Token.IDENTIFIER) {
          throw new ParserException("Syntax error: " + lexer.token());
        }

        String collate = lexer.stringVal();
        lexer.nextToken();

        expr =
            new SQLBinaryOpExpr(
                expr,
                SQLBinaryOperator.COLLATE,
                new SQLIdentifierExpr(collate),
                JdbcConstants.MYSQL);

        return primaryRest(expr);
      } else if (expr instanceof SQLVariantRefExpr) {
        if ("COLLATE".equalsIgnoreCase(lexer.stringVal())) {
          lexer.nextToken();

          if (lexer.token() != Token.IDENTIFIER) {
            throw new ParserException("Syntax error: " + lexer.token());
          }

          String collate = lexer.stringVal();
          lexer.nextToken();

          expr.putAttribute("COLLATE", collate);

          return primaryRest(expr);
        }
      } else if (expr instanceof SQLIntegerExpr) {
        SQLIntegerExpr intExpr = (SQLIntegerExpr) expr;
        String binaryString = lexer.stringVal();
        if (intExpr.getNumber().intValue() == 0 && binaryString.startsWith("b")) {
          lexer.nextToken();
          expr = new SQLBinaryExpr(binaryString.substring(1));

          return primaryRest(expr);
        }
      }
    }

    if (lexer.token() == Token.LPAREN && expr instanceof SQLIdentifierExpr) {
      SQLIdentifierExpr identExpr = (SQLIdentifierExpr) expr;
      String ident = identExpr.getName();

      if ("EXTRACT".equalsIgnoreCase(ident)) {
        lexer.nextToken();

        if (lexer.token() != Token.IDENTIFIER) {
          throw new ParserException("Syntax error: " + lexer.token());
        }

        String unitVal = lexer.stringVal();
        MySqlIntervalUnit unit = MySqlIntervalUnit.valueOf(unitVal.toUpperCase());
        lexer.nextToken();

        accept(Token.FROM);

        SQLExpr value = expr();

        MySqlExtractExpr extract = new MySqlExtractExpr();
        extract.setValue(value);
        extract.setUnit(unit);
        accept(Token.RPAREN);

        expr = extract;

        return primaryRest(expr);
      } else if ("SUBSTRING".equalsIgnoreCase(ident)) {
        lexer.nextToken();
        SQLMethodInvokeExpr methodInvokeExpr = new SQLMethodInvokeExpr(ident);
        for (; ; ) {
          SQLExpr param = expr();
          methodInvokeExpr.addParameter(param);

          if (lexer.token() == Token.COMMA) {
            lexer.nextToken();
            continue;
          } else if (lexer.token() == Token.FROM) {
            lexer.nextToken();
            SQLExpr from = expr();
            methodInvokeExpr.addParameter(from);

            if (lexer.token() == Token.FOR) {
              lexer.nextToken();
              SQLExpr forExpr = expr();
              methodInvokeExpr.addParameter(forExpr);
            }
            break;
          } else if (lexer.token() == Token.RPAREN) {
            break;
          } else {
            throw new ParserException("Syntax error: " + lexer.token());
          }
        }

        accept(Token.RPAREN);
        expr = methodInvokeExpr;

        return primaryRest(expr);
      } else if ("TRIM".equalsIgnoreCase(ident)) {
        lexer.nextToken();
        SQLMethodInvokeExpr methodInvokeExpr = new SQLMethodInvokeExpr(ident);

        if (lexer.token() == Token.IDENTIFIER) {
          String flagVal = lexer.stringVal();
          if ("LEADING".equalsIgnoreCase(flagVal)) {
            lexer.nextToken();
            methodInvokeExpr.getAttributes().put("TRIM_TYPE", "LEADING");
          } else if ("BOTH".equalsIgnoreCase(flagVal)) {
            lexer.nextToken();
            methodInvokeExpr.getAttributes().put("TRIM_TYPE", "BOTH");
          } else if ("TRAILING".equalsIgnoreCase(flagVal)) {
            lexer.nextToken();
            methodInvokeExpr.putAttribute("TRIM_TYPE", "TRAILING");
          }
        }

        SQLExpr param = expr();
        methodInvokeExpr.addParameter(param);

        if (lexer.token() == Token.FROM) {
          lexer.nextToken();
          SQLExpr from = expr();
          methodInvokeExpr.putAttribute("FROM", from);
        }

        accept(Token.RPAREN);
        expr = methodInvokeExpr;

        return primaryRest(expr);
      } else if ("MATCH".equalsIgnoreCase(ident)) {
        lexer.nextToken();
        MySqlMatchAgainstExpr matchAgainstExpr = new MySqlMatchAgainstExpr();

        if (lexer.token() == Token.RPAREN) {
          lexer.nextToken();
        } else {
          exprList(matchAgainstExpr.getColumns(), matchAgainstExpr);
          accept(Token.RPAREN);
        }

        acceptIdentifier("AGAINST");

        accept(Token.LPAREN);
        SQLExpr against = primary();
        matchAgainstExpr.setAgainst(against);

        if (lexer.token() == Token.IN) {
          lexer.nextToken();
          if (identifierEquals("NATURAL")) {
            lexer.nextToken();
            acceptIdentifier("LANGUAGE");
            acceptIdentifier("MODE");
            if (lexer.token() == Token.WITH) {
              lexer.nextToken();
              acceptIdentifier("QUERY");
              acceptIdentifier("EXPANSION");
              matchAgainstExpr.setSearchModifier(
                  MySqlMatchAgainstExpr.SearchModifier
                      .IN_NATURAL_LANGUAGE_MODE_WITH_QUERY_EXPANSION);
            } else {
              matchAgainstExpr.setSearchModifier(
                  MySqlMatchAgainstExpr.SearchModifier.IN_NATURAL_LANGUAGE_MODE);
            }
          } else if (identifierEquals("BOOLEAN")) {
            lexer.nextToken();
            acceptIdentifier("MODE");
            matchAgainstExpr.setSearchModifier(
                MySqlMatchAgainstExpr.SearchModifier.IN_BOOLEAN_MODE);
          } else {
            throw new ParserException("Syntax error: " + lexer.token());
          }
        } else if (lexer.token() == Token.WITH) {
          throw new ParserException("Syntax error: " + lexer.token());
        }

        accept(Token.RPAREN);

        expr = matchAgainstExpr;

        return primaryRest(expr);
      } else if ("CONVERT".equalsIgnoreCase(ident)) {
        lexer.nextToken();
        SQLMethodInvokeExpr methodInvokeExpr = new SQLMethodInvokeExpr(ident);

        if (lexer.token() != Token.RPAREN) {
          exprList(methodInvokeExpr.getParameters(), methodInvokeExpr);
        }

        if (identifierEquals("USING")) {
          lexer.nextToken();
          if (lexer.token() != Token.IDENTIFIER) {
            throw new ParserException("Syntax error: " + lexer.token());
          }
          String charset = lexer.stringVal();
          lexer.nextToken();
          methodInvokeExpr.putAttribute("USING", charset);
        }

        accept(Token.RPAREN);

        expr = methodInvokeExpr;

        return primaryRest(expr);
      } else if ("POSITION".equalsIgnoreCase(ident)) {
        accept(Token.LPAREN);
        SQLExpr subStr = this.primary();
        accept(Token.IN);
        SQLExpr str = this.expr();
        accept(Token.RPAREN);

        SQLMethodInvokeExpr locate = new SQLMethodInvokeExpr("LOCATE");
        locate.addParameter(subStr);
        locate.addParameter(str);

        expr = locate;
        return primaryRest(expr);
      }
    }

    if (lexer.token() == Token.VARIANT && "@".equals(lexer.stringVal())) {
      lexer.nextToken();
      MySqlUserName userName = new MySqlUserName();
      if (expr instanceof SQLCharExpr) {
        userName.setUserName(((SQLCharExpr) expr).toString());
      } else {
        userName.setUserName(((SQLIdentifierExpr) expr).getName());
      }

      if (lexer.token() == Token.LITERAL_CHARS) {
        userName.setHost("'" + lexer.stringVal() + "'");
      } else {
        userName.setHost(lexer.stringVal());
      }
      lexer.nextToken();
      return userName;
    }

    //
    if (expr instanceof SQLMethodInvokeExpr && lexer.token() == Token.LBRACKET) {
      lexer.nextToken();
      expr = bracketRest(expr);
      return primaryRest(expr);
    }

    if (lexer.token() == Token.ERROR) {
      throw new ParserException(
          "Syntax error, token: "
              + lexer.token()
              + " "
              + lexer.stringVal()
              + ", pos: "
              + lexer.pos());
    }

    /**
     * When the druid parser parses the quoted field in SELECT clause, e.g. SELECT `b`.`lastname`
     * FROM bank AS `b`, "`b`" is recognized as an identifier expr, and the token is DOT, then the
     * next identifier "`lastname`" would be recognized as the property name of "`b`". The parser
     * creates a SQLPropertyExpr with owner of "`b`" and property name of "`lastname`".
     *
     * <p>The following block of code prevents this specific case to generate SQLPropertyExpr, but
     * corrects the parser to generate a SQLIdentifierExpr with expr = "`b`.`lastname`".
     */
    if (lexer.token() == Token.DOT && expr instanceof SQLIdentifierExpr) {
      if (isQuoted(((SQLIdentifierExpr) expr).getName(), "`")) {
        lexer.nextToken();
        ((SQLIdentifierExpr) expr)
            .setName(((SQLIdentifierExpr) expr).getName() + "." + lexer.stringVal());
        lexer.nextToken();
      }
    }

    return super.primaryRest(expr);
  }

  protected SQLExpr bracketRest(SQLExpr expr) {
    Number index;

    if (lexer.token() == Token.LITERAL_INT) {
      index = lexer.integerValue();
      lexer.nextToken();
    } else {
      throw new ParserException("Syntax error : " + lexer.stringVal());
    }

    if (expr instanceof SQLMethodInvokeExpr) {
      SQLMethodInvokeExpr methodInvokeExpr = (SQLMethodInvokeExpr) expr;
      methodInvokeExpr.getParameters().add(new SQLIntegerExpr(index));
    }
    lexer.nextToken();
    expr = primaryRest(expr);
    return expr;
  }

  public SQLSelectParser createSelectParser() {
    return new ElasticSqlSelectParser(this);
  }

  protected SQLExpr parseInterval() {
    accept(Token.INTERVAL);

    if (lexer.token() == Token.LPAREN) {
      lexer.nextToken();

      SQLMethodInvokeExpr methodInvokeExpr = new SQLMethodInvokeExpr("INTERVAL");
      if (lexer.token() != Token.RPAREN) {
        exprList(methodInvokeExpr.getParameters(), methodInvokeExpr);
      }

      accept(Token.RPAREN);

      return primaryRest(methodInvokeExpr);
    } else {
      SQLExpr value = expr();

      if (lexer.token() != Token.IDENTIFIER) {
        throw new ParserException("Syntax error: " + lexer.token());
      }

      String unit = lexer.stringVal();
      lexer.nextToken();

      MySqlIntervalExpr intervalExpr = new MySqlIntervalExpr();
      intervalExpr.setValue(value);
      intervalExpr.setUnit(MySqlIntervalUnit.valueOf(unit.toUpperCase()));

      return intervalExpr;
    }
  }

  public SQLColumnDefinition parseColumn() {
    MySqlSQLColumnDefinition column = new MySqlSQLColumnDefinition();
    column.setName(name());
    column.setDataType(parseDataType());

    return parseColumnRest(column);
  }

  public SQLColumnDefinition parseColumnRest(SQLColumnDefinition column) {
    if (lexer.token() == Token.ON) {
      lexer.nextToken();
      accept(Token.UPDATE);
      SQLExpr expr = this.expr();
      ((MySqlSQLColumnDefinition) column).setOnUpdate(expr);
    }

    if (identifierEquals("AUTO_INCREMENT")) {
      lexer.nextToken();
      if (column instanceof MySqlSQLColumnDefinition) {
        ((MySqlSQLColumnDefinition) column).setAutoIncrement(true);
      }
      return parseColumnRest(column);
    }

    if (identifierEquals("precision")
        && column.getDataType().getName().equalsIgnoreCase("double")) {
      lexer.nextToken();
    }

    if (identifierEquals("PARTITION")) {
      throw new ParserException("syntax error " + lexer.token() + " " + lexer.stringVal());
    }

    if (identifierEquals("STORAGE")) {
      lexer.nextToken();
      SQLExpr expr = expr();
      if (column instanceof MySqlSQLColumnDefinition) {
        ((MySqlSQLColumnDefinition) column).setStorage(expr);
      }
    }

    super.parseColumnRest(column);

    return column;
  }

  protected SQLDataType parseDataTypeRest(SQLDataType dataType) {
    super.parseDataTypeRest(dataType);

    if (identifierEquals("UNSIGNED")) {
      lexer.nextToken();
      dataType.getAttributes().put("UNSIGNED", true);
    }

    if (identifierEquals("ZEROFILL")) {
      lexer.nextToken();
      dataType.getAttributes().put("ZEROFILL", true);
    }

    return dataType;
  }

  public SQLExpr orRest(SQLExpr expr) {

    for (; ; ) {
      if (lexer.token() == Token.OR || lexer.token() == Token.BARBAR) {
        lexer.nextToken();
        SQLExpr rightExp = and();

        expr =
            new SQLBinaryOpExpr(expr, SQLBinaryOperator.BooleanOr, rightExp, JdbcConstants.MYSQL);
      } else if (lexer.token() == Token.XOR) {
        lexer.nextToken();
        SQLExpr rightExp = and();

        expr =
            new SQLBinaryOpExpr(expr, SQLBinaryOperator.BooleanXor, rightExp, JdbcConstants.MYSQL);
      } else {
        break;
      }
    }

    return expr;
  }

  public SQLExpr additiveRest(SQLExpr expr) {
    if (lexer.token() == Token.PLUS) {
      lexer.nextToken();
      SQLExpr rightExp = multiplicative();

      expr = new SQLBinaryOpExpr(expr, SQLBinaryOperator.Add, rightExp, JdbcConstants.MYSQL);
      expr = additiveRest(expr);
    } else if (lexer.token() == Token.SUB) {
      lexer.nextToken();
      SQLExpr rightExp = multiplicative();

      expr = new SQLBinaryOpExpr(expr, SQLBinaryOperator.Subtract, rightExp, JdbcConstants.MYSQL);
      expr = additiveRest(expr);
    }

    return expr;
  }

  public SQLAssignItem parseAssignItem() {
    SQLAssignItem item = new SQLAssignItem();

    SQLExpr var = primary();

    String ident = null;
    if (var instanceof SQLIdentifierExpr) {
      ident = ((SQLIdentifierExpr) var).getName();

      if ("GLOBAL".equalsIgnoreCase(ident)) {
        ident = lexer.stringVal();
        lexer.nextToken();
        var = new SQLVariantRefExpr(ident, true);
      } else if ("SESSION".equalsIgnoreCase(ident)) {
        ident = lexer.stringVal();
        lexer.nextToken();
        var = new SQLVariantRefExpr(ident, false);
      } else {
        var = new SQLVariantRefExpr(ident);
      }
    }

    if ("NAMES".equalsIgnoreCase(ident)) {
      // skip
    } else if ("CHARACTER".equalsIgnoreCase(ident)) {
      var = new SQLIdentifierExpr("CHARACTER SET");
      accept(Token.SET);
      if (lexer.token() == Token.EQ) {
        lexer.nextToken();
      }
    } else {
      if (lexer.token() == Token.COLONEQ) {
        lexer.nextToken();
      } else {
        accept(Token.EQ);
      }
    }

    item.setValue(this.expr());

    item.setTarget(var);
    return item;
  }

  public SQLName nameRest(SQLName name) {
    if (lexer.token() == Token.VARIANT && "@".equals(lexer.stringVal())) {
      lexer.nextToken();
      MySqlUserName userName = new MySqlUserName();
      userName.setUserName(((SQLIdentifierExpr) name).getName());

      if (lexer.token() == Token.LITERAL_CHARS) {
        userName.setHost("'" + lexer.stringVal() + "'");
      } else {
        userName.setHost(lexer.stringVal());
      }
      lexer.nextToken();
      return userName;
    }
    return super.nameRest(name);
  }

  public MySqlSelectQueryBlock.Limit parseLimit() {
    if (lexer.token() == Token.LIMIT) {
      lexer.nextToken();

      MySqlSelectQueryBlock.Limit limit = new MySqlSelectQueryBlock.Limit();

      SQLExpr temp = this.expr();
      if (lexer.token() == (Token.COMMA)) {
        limit.setOffset(temp);
        lexer.nextToken();
        limit.setRowCount(this.expr());
      } else if (identifierEquals("OFFSET")) {
        limit.setRowCount(temp);
        lexer.nextToken();
        limit.setOffset(this.expr());
      } else {
        limit.setRowCount(temp);
      }
      return limit;
    }

    return null;
  }

  @Override
  public MySqlPrimaryKey parsePrimaryKey() {
    accept(Token.PRIMARY);
    accept(Token.KEY);

    MySqlPrimaryKey primaryKey = new MySqlPrimaryKey();

    if (identifierEquals("USING")) {
      lexer.nextToken();
      primaryKey.setIndexType(lexer.stringVal());
      lexer.nextToken();
    }

    accept(Token.LPAREN);
    for (; ; ) {
      primaryKey.getColumns().add(this.expr());
      if (!(lexer.token() == (Token.COMMA))) {
        break;
      } else {
        lexer.nextToken();
      }
    }
    accept(Token.RPAREN);

    return primaryKey;
  }

  public MySqlUnique parseUnique() {
    accept(Token.UNIQUE);

    if (lexer.token() == Token.KEY) {
      lexer.nextToken();
    }

    if (lexer.token() == Token.INDEX) {
      lexer.nextToken();
    }

    MySqlUnique unique = new MySqlUnique();

    if (lexer.token() != Token.LPAREN) {
      SQLName indexName = name();
      unique.setIndexName(indexName);
    }

    accept(Token.LPAREN);
    for (; ; ) {
      unique.getColumns().add(this.expr());
      if (!(lexer.token() == (Token.COMMA))) {
        break;
      } else {
        lexer.nextToken();
      }
    }
    accept(Token.RPAREN);

    if (identifierEquals("USING")) {
      lexer.nextToken();
      unique.setIndexType(lexer.stringVal());
      lexer.nextToken();
    }

    return unique;
  }

  public MysqlForeignKey parseForeignKey() {
    accept(Token.FOREIGN);
    accept(Token.KEY);

    MysqlForeignKey fk = new MysqlForeignKey();

    if (lexer.token() != Token.LPAREN) {
      SQLName indexName = name();
      fk.setIndexName(indexName);
    }

    accept(Token.LPAREN);
    this.names(fk.getReferencingColumns());
    accept(Token.RPAREN);

    accept(Token.REFERENCES);

    fk.setReferencedTableName(this.name());

    accept(Token.LPAREN);
    this.names(fk.getReferencedColumns());
    accept(Token.RPAREN);

    if (identifierEquals("MATCH")) {
      if (identifierEquals("FULL")) {
        fk.setReferenceMatch(MysqlForeignKey.Match.FULL);
      } else if (identifierEquals("PARTIAL")) {
        fk.setReferenceMatch(MysqlForeignKey.Match.PARTIAL);
      } else if (identifierEquals("SIMPLE")) {
        fk.setReferenceMatch(MysqlForeignKey.Match.SIMPLE);
      }
    }

    if (lexer.token() == Token.ON) {
      lexer.nextToken();
      if (lexer.token() == Token.DELETE) {
        fk.setReferenceOn(MysqlForeignKey.On.DELETE);
      } else if (lexer.token() == Token.UPDATE) {
        fk.setReferenceOn(MysqlForeignKey.On.UPDATE);
      } else {
        throw new ParserException(
            "Syntax error, expect DELETE or UPDATE, actual "
                + lexer.token()
                + " "
                + lexer.stringVal());
      }
      lexer.nextToken();

      if (lexer.token() == Token.RESTRICT) {
        fk.setReferenceOption(MysqlForeignKey.Option.RESTRICT);
      } else if (identifierEquals("CASCADE")) {
        fk.setReferenceOption(MysqlForeignKey.Option.CASCADE);
      } else if (lexer.token() == Token.SET) {
        accept(Token.NULL);
        fk.setReferenceOption(MysqlForeignKey.Option.SET_NULL);
      } else if (identifierEquals("ON")) {
        lexer.nextToken();
        if (identifierEquals("ACTION")) {
          fk.setReferenceOption(MysqlForeignKey.Option.NO_ACTION);
        } else {
          throw new ParserException(
              "Syntax error, expect ACTION, actual " + lexer.token() + " " + lexer.stringVal());
        }
      }
      lexer.nextToken();
    }
    return fk;
  }

  protected SQLAggregateExpr parseAggregateExprRest(SQLAggregateExpr aggregateExpr) {
    if (lexer.token() == Token.ORDER) {
      SQLOrderBy orderBy = this.parseOrderBy();
      aggregateExpr.putAttribute("ORDER BY", orderBy);
    }
    if (identifierEquals("SEPARATOR")) {
      lexer.nextToken();

      SQLExpr seperator = this.primary();

      aggregateExpr.putAttribute("SEPARATOR", seperator);
    }
    return aggregateExpr;
  }

  public MySqlSelectGroupByExpr parseSelectGroupByItem() {
    MySqlSelectGroupByExpr item = new MySqlSelectGroupByExpr();

    item.setExpr(expr());

    if (lexer.token() == Token.ASC) {
      lexer.nextToken();
      item.setType(SQLOrderingSpecification.ASC);
    } else if (lexer.token() == Token.DESC) {
      lexer.nextToken();
      item.setType(SQLOrderingSpecification.DESC);
    }

    return item;
  }
}
